"""
Copyright 2008 VMware, Inc.  All rights reserved. -- VMware Confidential
"""
import os
import traceback

from functools import partial

from vmis import core, VERSION, INSTALLER_INTERNAL
from vmis.db import db
from vmis.util import wrap
from vmis.util.log import getLog
from vmis.util.path import path
from vmis.core import install
from vmis.core.component import InstalledComponent
from vmis.core.errors import AbortError
from vmis.core.files import BINDIR, CONFDIR
from vmis.core.version import Version

# Here to stop a dependency loop.
from vmis.core.repository import Repository

log = getLog('vmis.core.common')

def ParseExceptionTuple(excTuple):
   """
   Parse the tuple created by sys.exc_info() and return the type of
   the exception as a string.

   @param excTuple: The output from sys.exc_info()
   @return: The exception name as a string
   """
   (excType, excValue, tback) = excTuple
   strList = traceback.format_exception(excType, excValue, tback)
   log.error('Uncaught exception in installer:\n%s' % ''.join(strList))
   typ = excType.__name__
   return typ

class State(object):
   """ Base abstract state """
   @staticmethod
   def Initialize(txn, state):
      """
      Initialize the state for the given action

      For the GTK+ UI this is the last chance to do anything before
      the callback returns.
      """
      pass

   @staticmethod
   def Abort(txn):
      """ Abort the transaction """
      def func():
         raise AbortError('Installation was aborted')

      txn.put(func)

# XXX: This is here to keep out a dependency loop.  See if that loop
#  can be fixed.
class Install(State):
   @staticmethod
   def Initialize(txn, state):
      if txn.installMode == 'Installation':
         txn.ui.SetPrimaryText('Installing')
         txn.ui.EnableBack(False)
      else:
         txn.ui.SetPrimaryText('Uninstalling')
         txn.ui.HideBack()
         txn.ui.HideCancel()

      # In both case, the 'Next' button is needed,
      # it becomes 'Close' on the 'Finish' screen.
      txn.ui.EnableNext(False)
      txn.ui.SetSecondaryText('Please wait...')

   @staticmethod
   def Abort(txn):
      """ Abort the installation """
      core.ABORTED = True

   @classmethod
   def Show(cls, txn, actions):
      uninstallActions, installActions, bonusChangeActions = actions

      # Initialize the installation now that all questions have been
      # asked.  Since we are not yet installing, let's say we are
      # preparing
      for i in installActions:
         txn.ui.SetPrimaryText('Preparing')

         i.Initialize(txn.temp)
         txn.count += i.Count()

      onProgress = partial(cls._onProgress, txn)
      txn.ui.ShowProgress()

      for u in uninstallActions:
         # Disable cancellation while uninstalls are in progress
         txn.ui.EnableCancel(False)
         txn.ui.SetPrimaryText('Uninstalling')

         txn.ui.SetPrimaryProgressMessage(u'Uninstalling %s %s' % (u.component.longName, u.component.version))
         wrap(u.PreUninstall, txn.opts['ignoreErrors'])

         txn.ui.SetSecondaryProgressMessage(u'Removing files...')
         u.Execute(txn.temp, onProgress)

         # Now run the debonusing actions before we remove too much from the
         # database.  Doing it later will be more difficult.
         for b in bonusChangeActions[:]:
            if b.component.SameAs(u.component) and not b.bonus:
               b.Execute()
               bonusChangeActions.remove(b)

         txn.ui.SetSecondaryProgressMessage(u'Deconfiguring...')
         wrap(u.PostUninstall, txn.opts['ignoreErrors'], onProgress)

         db.database.Commit()

      # Wrap up any components that lost a bonus but were not uninstalled.
      for b in bonusChangeActions[:]:
         if not b.bonus:
            b.Execute()
            bonusChangeActions.remove(b)

      i = None
      try:
         for i in installActions:
            # Re-enable the cancel button now that installation is beginning.
            txn.ui.EnableCancel(True)
            txn.ui.SetPrimaryText('Installing')

            txn.ui.SetPrimaryProgressMessage(u'Installing %s %s' % (i.component.longName, i.component.version))
            wrap(i.PreInstall, txn.opts['ignoreErrors'])

            txn.ui.SetSecondaryProgressMessage(u'Copying files...')
            i.Execute(txn.temp, onProgress)

            txn.ui.SetSecondaryProgressMessage(u'Configuring...')
            wrap(i.PostInstall, txn.opts['ignoreErrors'])

            # If we've just upgraded an installer, we need to make sure that
            # the core links are reassigned to this new one.  If we don't, then
            # the existing components' core_version won't point to the right
            # component ID anymore.
            if i.component.name == 'vmware-installer' and i.upgrade:
               db.components.RemapCoreVersions(i.old.uid, i.component.uid)

            db.database.Commit()

            # XXX: This is running the PostTransaction right after the
            # component is installed, unlike the PreTransaction which
            # runs them all at once.  This is so that the messages
            # about the current component being installed are accurate.
            wrap(i.PostTransaction, txn.opts['ignoreErrors'])

         # Give a bonus to any components that require it.  This also
         # creates uninstall scripts for these products.
         for b in bonusChangeActions[:]:
            if b.bonus:
               b.Execute()
               bonusChangeActions.remove(b)

      except:
         # Attempt to rollback the current component.  It is
         # imperative that nothing here unexpectedly throw an exception.
         # Otherwise, files will be left behind on the system but
         # their registrations in the database will be lost and never
         # removed (unless another installation/uninstallation is
         # run).

         # uid isn't set until Execute() so we cannot guarantee it to
         # exist.  Perhaps we should track which stage of the install
         # failed more closely instead of blindly running everything.
         if not i.component.uid:
            raise

         log.error('[%s %s] Installation failed, rolling back installation.',
                   i.component.name, i.component.version)

         # XXX: This is potentially hazardous if this gets run on
         # vmware-installer because the database will end up being
         # deleted.
         un = install.Uninstall(InstalledComponent(db.database, i.component.uid),
                                db.database,
                                old=i.component,
                                new=0)
         txn.ui.SetPrimaryProgressMessage(u'Rolling back %s %s' %
                                          (un.component.longName, un.component.version))

         wrap(un.Load, True, txn.temp)
         wrap(un.Initialize, True, txn.temp)
         wrap(un.PreUninstall, ignoreErrors=True)

         txn.ui.SetSecondaryProgressMessage(u'Removing files...')
         un.Execute(txn.temp, onProgress)

         txn.ui.SetSecondaryProgressMessage(u'Deconfiguring...')
         wrap(un.PostUninstall, True, onProgress)

         db.database.Commit()

         raise

      db.database.Commit()
      # XXX: This is a quick fix to guarantee that the installation
      # bar completes at 100%.  It doesn't fix the underlying problem,
      # but is the fix we're going to use for Iron unless there is time
      # to do something better.
      txn.ui.SetProgress(1.0)
      txn.Next()

   @staticmethod
   def _onProgress(txn):
      fraction = txn.currentCount * 1.0 / txn.count
      txn.ui.SetProgress(fraction)

      # Start counting backwards if we've aborted the installation.
      if core.ABORTED:
         txn.currentCount -= 1
      else:
         txn.currentCount += 1

# XXX: This is here in common to provide the global repository and
#  weed out a dependency loop.  Remove this if at all possible.
# It shows that there is a cycle in the file dependencies that
#  should be fixed.  It used to reside in db.py, but was moved
#  due to a dependency loop.
#
# db -> repository.py -> component.py -> files.py ->
#       db
#
# Break this loop somewhere and fix this if possible.
# I'd like to still associate a repository with a database without
#  explicitly calling SetRepository
repository = None

def SetRepository(dbase):
   global repository

   repository = Repository(dbase)

# XXX: We can't use path here since in vmware-installer.py, the
# database has not yet been initialized when it is checking these
# values.  If a path *is* here, it will try to expand it with a
# nonexistant DB.
SYSTEM_BOOTSTRAP = path('/etc/vmware-installer/bootstrap')
SYSTEM_DATABASE = path('/etc/vmware-installer/database')

